
package com.xiashitech.apiservice.api;

import static com.xiashitech.apiservice.Constants.ErrorCode.*;
import com.xiashitech.apiservice.util.*;
import com.xiashitech.interfaces.apiservice.MockRowDataGenerator;
import com.xiashitech.interfaces.apiservice.dto.user.*;
import lombok.RequiredArgsConstructor;
import lombok.var;

import com.xiashitech.apiservice.service.*;
import com.xiashitech.apiservice.config.AuthConfig;
import com.xiashitech.apiservice.dao.model.*;
import org.springframework.stereotype.Controller;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.alibaba.fastjson.*;

import java.io.IOException;
import java.util.*;
import java.util.regex.*;
import java.util.stream.Collectors;
import com.xiashitech.apiservice.dto.Result;
import org.springframework.web.multipart.MultipartFile;
import static com.xiashitech.apiservice.Constants.FileType.*;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.core.util.*;
import cn.hutool.log.*;
import cn.hutool.core.collection.*;
import cn.hutool.core.convert.NumberChineseFormatter;

@Controller
@RequestMapping("v1")
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class DataWaterMarkController {
    private final AuthConfig authConfig;
    private static final Log log = LogFactory.get();
    private final IUserInfoService userInfoService;
    private final IDataWaterMarkService dataWaterMarkService;
    private final IDataWaterMarkConfigService configService;
    private static final String API_PATH_PATTERN = "$.upstreamEdges[toEntity = '%s'].lineageDetails.columnsLineage[toColumn like '%%%s']";
    private static final String TABLE_PATH_PATTERN = "$.columns[name = '%s'].tags[0].tagFQN";

    @RequestMapping(value = "agent/watermark/{apiConfigId}/{appName}/{userId}", method = RequestMethod.POST)
    @ResponseBody
    public Result<JSONArray> addDataWaterMark(@RequestBody JSONArray data, @PathVariable Long apiConfigId,
            @PathVariable String userId, @PathVariable String appName) {
        var currentDate = new Date();
        log.info("Enter addDataWaterMark configId:{},appName:{},dataSize:{}", apiConfigId, appName, data.size());
        Result<JSONArray> result = Result.failure();
        var newUserId = String.format("%s@%s", userId, appName);
        // 用户基本信息
        var userInfo = getUserInfo(newUserId);
        if (Objects.isNull(userInfo))
            return Result.failure(RESULT_5002);
        if (!userInfo.getResult())
            return result.setStatus(userInfo.getCode()).setMessage(userInfo.getMessage());
        // api配置信息
        var apiConfig = configService.getDataWaterMarkConfigById(apiConfigId);
        if (Objects.isNull(apiConfig))
            return Result.failure(RESULT_5003);
        var keys = JsonIoUtil.getKeysOfObjectWithMostKeys(data);
        if (CollUtil.isEmpty(keys))
            log.info("addDataWaterMark: keys is empty");
        MockRowDataGenerator generator = new HybridDataGenerator();
        var insertIndex = apiConfig.getMorethanLine();
        var transformed = generator.processMockRowData(insertIndex, data);
        if (data.size() != transformed.size() - 1)
            return Result.failure(RESULT_5004);
        var dataWaterMark = buildDataWaterMark(apiConfigId, userId, appName,
                StringUtils.join(keys, ","), currentDate,
                userInfo, apiConfig, insertIndex, transformed);
        var affectedRows = dataWaterMarkService.insertDataWaterMark(dataWaterMark);
        if (affectedRows > 0)
            result = Result.success(transformed);
        log.info("Exit addDataWaterMark apiPath:{},appName:{},id:{} result->code:{},message:{},dataSize:{}",
                apiConfig.getApiPath(), appName, dataWaterMark.getId(), result.getStatus(), result.getMessage(),
                Objects.nonNull(result.getData()) ? result.getData().size() : -1);
        return result;
    }

    /***
     * 
     * @param data
     * @param apiConfigId
     * @param userId
     * @param appName
     * @param keys
     * @param date
     * @param userInfo
     * @param apiConfig
     * @param insertIndex
     * @param transformed
     * @return
     */
    private DataWaterMark buildDataWaterMark(Long apiConfigId, String userId, String appName,
            String keys, Date date,
            UserInfoResponse userInfo, DataWaterMarkConfig apiConfig, int insertIndex, JSONArray data) {
        var rowData = JSON.toJSONString(data.get(insertIndex));
        var content = new JSONObject(true);
        content.put("line", insertIndex + 1);
        content.put("mock", rowData);

        data = JsonIoUtil.formatInData(data);
        var dataWaterMark = new DataWaterMark();
        dataWaterMark.setUserId(Long.parseLong(userId));
        dataWaterMark.setUsername(userInfo.getData().getUsername());
        dataWaterMark.setAppName(appName);
        dataWaterMark.setContent(JSON.toJSONString(content));
        dataWaterMark.setInfoSummary(DigestUtil.md5Hex(JSON.toJSONString(data)));
        dataWaterMark.setRowSummary(DigestUtil.md5Hex(rowData));
        dataWaterMark.setApiConfigId(apiConfigId);
        dataWaterMark.setJsonKeys(keys);
        dataWaterMark.setApiPath(apiConfig.getApiPath());
        dataWaterMark.setRowCount(data.size() - 1);
        dataWaterMark.setCreateDate(date);
        return dataWaterMark;
    }

    /**
     * 
     * @param userId
     * @return
     */
    private UserInfoResponse getUserInfo(String userId) {
        var userRequest = new UserInfoRequest();
        userRequest.setId(userId);
        var userInfo = Optional.ofNullable(userInfoService.retrieveUserInfo(userRequest));
        return userInfo.orElse(null);
    }

    @RequestMapping(value = "server/watermark/{id}", method = RequestMethod.POST)
    @ResponseBody
    public JSONArray removeDataWaterMark(@RequestBody JSONArray data, @PathVariable Long id) {
        var dataWaterMark = Optional
                .ofNullable(dataWaterMarkService.getDataWaterMarkById(id));
        dataWaterMark.ifPresent(item -> {
            var content = JSONObject.parseObject(item.getContent());
            var line = content.get("line").toString();
            if (line.matches("-?\\d+")) {
                var lineNum = Integer.parseInt(line);
                data.remove(lineNum - 1);
            }
        });
        return data;
    }

    @RequestMapping(value = "server/verifysign", method = RequestMethod.POST)
    @ResponseBody
    public Result<List<DataWaterMark>> verifySignature(@RequestParam("file") MultipartFile file) throws IOException {
        Result<List<DataWaterMark>> result = Result.failure();
        var contentType = FileUtil.getContentType(file);
        switch (contentType) {
            case CONTENT_TYPE_XLS:
            case CONTENT_TYPE_XLSX:
                getDataInfo(XlsIoUtil.parseXls(file), result);
                break;
            case CONTENT_TYPE_CSV:
                getDataInfo(CsvIoUtil.parseCsv(file), result);
                break;
            case CONTENT_TYPE_JSON:
                getDataInfo(JsonIoUtil.parseJson(file), result);
                break;
            default:
                result = Result.failure(RESULT_5001);
                break;
        }
        if (result.getStatus() == Integer.parseInt(RESULT_1001.getCode())) {
            var data = result.getData();
            if (CollUtil.isEmpty(data)) {
                log.info("verifySignature: data don't exist");
                return result;
            }
            for (var d : data) {
                var dataWaterMarkConfig = configService
                        .getDataWaterMarkConfigByPath(d.getApiPath());
                if (Objects.isNull(dataWaterMarkConfig)) {
                    continue;
                }
                hidePartInfo(result, dataWaterMarkConfig.getFlag());
            }
        }
        return result;
    }

    /**
     * 得到水印信息记录
     */
    private void getDataInfo(List<?> data, Result<List<DataWaterMark>> result) {
        if (CollUtil.isEmpty(data))
            return;
        var conditionMet = ArrayUtil.isArray(data.get(0)) || data.get(0) instanceof List;
        if (!conditionMet)
            return;
        var listOfStrings = data.stream().map(item -> JSONArray.toJSONString(item)).collect(Collectors.toList());
        var content = String.join(",", listOfStrings);
        var infoSummary = DigestUtil.md5Hex(String.format("[%s]", content));
        if (infoSummary.isEmpty()) {
            log.info("getDataInfo: infoSummary is empty!");
            return;
        }
        var conditionItem = new DataWaterMark();
        conditionItem.setInfoSummary(infoSummary);
        var item = dataWaterMarkService.getDataWaterMarkList(conditionItem);
        if (CollUtil.isNotEmpty(item))
            result.setStatus(Integer.parseInt(RESULT_1001.getCode())).setData(item)
                    .setMessage("verification successful.");
    }

    /**
     * 隐藏部分记录信息
     *
     * @param result 水印数据的返回值
     * @param flag   隐藏标记位如11000(5位长度）
     */
    private void hidePartInfo(Result<List<DataWaterMark>> result, String flag) {
        var data = result.getData();
        if (CollUtil.isEmpty(result.getData()))
            return;
        for (var d : data) {
            var status = flag.charAt(0);
            if (status == '0') {
                d.setAppName("unknown");
            }
            status = flag.charAt(1);
            if (status == '0') {
                d.setUsername("unknown");
            }
            status = flag.charAt(2);
            if (status == '0') {
                d.setCreateDate(null);
            }
            status = flag.charAt(3);
            if (status == '0') {
                d.setRowCount(-1);
            }
            status = flag.charAt(4);
            if (status == '0') {
                d.setDataLevel(-1);
            } else {
                d.setDataLevel(getDataLevel(d));
            }
        }
    }

    private int getDataLevel(DataWaterMark dataWaterMark) {
        var result = -1;
        var apiPath = dataWaterMark.getApiPath().replace('/', '_');
        var appName = dataWaterMark.getAppName();
        var jsonKeys = dataWaterMark.getJsonKeys();
        if (StringUtils.isAnyBlank(appName, apiPath, jsonKeys))
            return result;
        var apiFqn = String.format("ApiService.%s.%s", appName, apiPath);
        var apiUrl = authConfig.getMetaDataUrlForApi().replace("{}", apiFqn);
        var hr = HttpRequest.get(apiUrl).form("upstreamDepth", 2).form("downstreamDepth", 2)
                .timeout(30 * 1000).execute();
        var json = hr.body();
        if (!hr.isOk() || !cn.hutool.json.JSONUtil.isTypeJSON(json))
            return result;
        var id = JSONPath.read(json, "$.entity.id");
        var keys = jsonKeys.split(",");
        if (ObjectUtil.isEmpty(id) || ArrayUtil.isEmpty(keys))
            return result;
        for (int i = 0; i < keys.length; i++) {
            var columnsLines = (List<Object>) JSONPath.eval(json, String.format(API_PATH_PATTERN, id, keys[i]));
            if (CollUtil.isEmpty(columnsLines))
                continue;
            var mappingItems = new JSONArray(columnsLines);
            for (var item : mappingItems) {
                var jsonObj = (JSONObject) item;
                var tableFqns = jsonObj.getJSONArray("fromColumns");
                if (CollUtil.isEmpty(tableFqns))
                    continue;
                result = getTableLevel(result, tableFqns);
            }
        }
        return result;
    }

    /**
     * 
     */
    private int getTableLevel(int result, JSONArray tableFqns) {
        for (var f : tableFqns) {
            var lastIndeOf = f.toString().lastIndexOf(".");
            var ff = f.toString().substring(0, lastIndeOf);
            var url = authConfig.getMetaDataUrlForTable1().replace("{}", ff.toString());
            var hr = HttpRequest.get(url).timeout(30 * 1000).execute();
            var json = hr.body();
            if (hr.isOk() && cn.hutool.json.JSONUtil.isTypeJSON(json)) {
                var path = String.format(TABLE_PATH_PATTERN, f.toString().substring(lastIndeOf + 1));
                var tagFQN = JSONPath.eval(json, path);
                if (Objects.isNull(tagFQN) || StrUtil.isBlank(tagFQN.toString()))
                    continue;
                var level = NumberChineseFormatter.chineseToNumber(extractChineseNumber(tagFQN.toString()));
                if (level > result)
                    result = level;
            }
        }
        return result;
    }

    private static String extractChineseNumber(String input) {
        var pattern = Pattern.compile("[一二三四五六七八九十百千万]+");
        var matcher = pattern.matcher(input);
        if (matcher.find()) {
            return matcher.group();
        } else {
            return "";
        }
    }
}